/*
 * Copyright (C) 2013 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.picasso;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.widget.ImageView;
import android.widget.RemoteViews;

import com.squareup.picasso.Action.RequestWeakReference;

import java.io.File;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;

import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static com.squareup.picasso.Dispatcher.HUNTER_BATCH_COMPLETE;
import static com.squareup.picasso.Dispatcher.REQUEST_BATCH_RESUME;
import static com.squareup.picasso.Dispatcher.REQUEST_GCED;
import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
import static com.squareup.picasso.Utils.OWNER_MAIN;
import static com.squareup.picasso.Utils.THREAD_PREFIX;
import static com.squareup.picasso.Utils.VERB_CANCELED;
import static com.squareup.picasso.Utils.VERB_COMPLETED;
import static com.squareup.picasso.Utils.VERB_ERRORED;
import static com.squareup.picasso.Utils.VERB_RESUMED;
import static com.squareup.picasso.Utils.checkMain;
import static com.squareup.picasso.Utils.log;

/**
 * Image downloading, transformation, and caching manager.
 * <p/>
 * Use {@link #with(android.content.Context)} for the global singleton instance
 * or construct your own instance with {@link com.squareup.picasso.Picasso.Builder}.
 */
public class Picasso {

    /**
     * Callbacks for Picasso events.
     */
    public interface Listener {
        /**
         * Invoked when an image has failed to load. This is useful for
         * reporting image failures to a remote analytics service, for example.
         */
        void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception);
    }

    /**
     * A transformer that is called immediately before every request is
     * submitted. This can be used to modify any information about a request.
     * <p/>
     * For example, if you use a CDN you can change the hostname for the image
     * based on the current location of the user in order to get faster download
     * speeds.
     * <p/>
     * <b>NOTE:</b> This is a beta feature. The API is subject to change in a
     * backwards incompatible way at any time.
     */
    public interface RequestTransformer {
        /**
         * Transform a request before it is submitted to be processed.
         *
         * @return The original request or a new request to replace it. Must not
         * be null.
         */
        Request transformRequest(Request request);

        /**
         * A {@link com.squareup.picasso.Picasso.RequestTransformer} which returns the original request.
         */
        RequestTransformer IDENTITY = new RequestTransformer() {
            @Override
            public Request transformRequest(Request request) {
                return request;
            }
        };
    }

    /**
     * The priority of a request.
     *
     * @see com.squareup.picasso.RequestCreator#priority(com.squareup.picasso.Picasso.Priority)
     */
    public enum Priority {
        LOW, NORMAL, HIGH
    }

    static final String TAG = "Picasso";
    static final Handler HANDLER = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case HUNTER_BATCH_COMPLETE: {
                    @SuppressWarnings("unchecked")
                    List<BitmapHunter> batch = (List<BitmapHunter>) msg.obj;
                    // noinspection ForLoopReplaceableByForEach
                    for (int i = 0, n = batch.size(); i < n; i++) {
                        BitmapHunter hunter = batch.get(i);
                        hunter.picasso.complete(hunter);
                    }
                    break;
                }
                case REQUEST_GCED: {
                    Action action = (Action) msg.obj;
                    if (action.getPicasso().loggingEnabled) {
                        log(OWNER_MAIN, VERB_CANCELED, action.request.logId(), "target got garbage collected");
                    }
                    action.picasso.cancelExistingRequest(action.getTarget());
                    break;
                }
                case REQUEST_BATCH_RESUME:
                    @SuppressWarnings("unchecked")
                    List<Action> batch = (List<Action>) msg.obj;
                    for (int i = 0, n = batch.size(); i < n; i++) {
                        Action action = batch.get(i);
                        action.picasso.resumeAction(action);
                    }
                    break;
                default:
                    throw new AssertionError("Unknown handler message received: " + msg.what);
            }
        }
    };

    static Picasso singleton = null;

    private final Listener listener;
    private final RequestTransformer requestTransformer;
    private final CleanupThread cleanupThread;
    private final List<RequestHandler> requestHandlers;

    final Context context;
    final Dispatcher dispatcher;
    final Cache cache;
    final Stats stats;
    /**
     * 显示组件（ImageView）与Action的关系配对
     */
    final Map<Object, Action> targetToAction;
    final Map<ImageView, DeferredRequestCreator> targetToDeferredRequestCreator;
    final ReferenceQueue<Object> referenceQueue;

    boolean indicatorsEnabled;
    volatile boolean loggingEnabled;

    boolean shutdown;

    Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,
            RequestTransformer requestTransformer, List<RequestHandler> extraRequestHandlers, Stats stats,
            boolean indicatorsEnabled, boolean loggingEnabled) {
        this.context = context;
        this.dispatcher = dispatcher;
        this.cache = cache;
        this.listener = listener;
        this.requestTransformer = requestTransformer;

        int builtInHandlers = 7; // Adjust this as internal handlers are added
        // or removed.
        int extraCount = (extraRequestHandlers != null ? extraRequestHandlers.size() : 0);
        List<RequestHandler> allRequestHandlers = new ArrayList<RequestHandler>(builtInHandlers + extraCount);

        // ResourceRequestHandler needs to be the first in the list to avoid
        // forcing other RequestHandlers to perform null checks on request.uri
        // to cover the (request.resourceId != 0) case.
        allRequestHandlers.add(new ResourceRequestHandler(context));
        if (extraRequestHandlers != null) {
            allRequestHandlers.addAll(extraRequestHandlers);
        }
        allRequestHandlers.add(new ContactsPhotoRequestHandler(context));
        allRequestHandlers.add(new MediaStoreRequestHandler(context));
        allRequestHandlers.add(new ContentStreamRequestHandler(context));
        allRequestHandlers.add(new AssetRequestHandler(context));
        allRequestHandlers.add(new FileRequestHandler(context));
        allRequestHandlers.add(new NetworkRequestHandler(dispatcher.downloader, stats));
        requestHandlers = Collections.unmodifiableList(allRequestHandlers);

        this.stats = stats;
        this.targetToAction = new WeakHashMap<Object, Action>();
        this.targetToDeferredRequestCreator = new WeakHashMap<ImageView, DeferredRequestCreator>();
        this.indicatorsEnabled = indicatorsEnabled;
        this.loggingEnabled = loggingEnabled;
        this.referenceQueue = new ReferenceQueue<Object>();
        this.cleanupThread = new CleanupThread(referenceQueue, HANDLER);
        this.cleanupThread.start();
    }

    /**
     * Cancel any existing requests for the specified target {@link android.widget.ImageView}.
     */
    public void cancelRequest(ImageView view) {
        cancelExistingRequest(view);
    }

    /**
     * Cancel any existing requests for the specified {@link com.squareup.picasso.Target} instance.
     */
    public void cancelRequest(Target target) {
        cancelExistingRequest(target);
    }

    /**
     * Cancel any existing requests for the specified {@link android.widget.RemoteViews} target
     * with the given {@code viewId}.
     */
    public void cancelRequest(RemoteViews remoteViews, int viewId) {
        cancelExistingRequest(new RemoteViewsAction.RemoteViewsTarget(remoteViews, viewId));
    }

    /**
     * Cancel any existing requests with given tag. You can set a tag on new
     * requests with {@link com.squareup.picasso.RequestCreator#tag(Object)}.
     *
     * @see com.squareup.picasso.RequestCreator#tag(Object)
     */
    public void cancelTag(Object tag) {
        checkMain();
        List<Action> actions = new ArrayList<Action>(targetToAction.values());
        for (int i = 0, n = actions.size(); i < n; i++) {
            Action action = actions.get(i);
            if (action.getTag().equals(tag)) {
                cancelExistingRequest(action.getTarget());
            }
        }
    }

    /**
     * Pause existing requests with the given tag. Use
     * {@link #resumeTag(Object)} to resume requests with the given tag.
     *
     * @see #resumeTag(Object)
     * @see com.squareup.picasso.RequestCreator#tag(Object)
     */
    public void pauseTag(Object tag) {
        dispatcher.dispatchPauseTag(tag);
    }

    /**
     * Resume paused requests with the given tag. Use {@link #pauseTag(Object)}
     * to pause requests with the given tag.
     *
     * @see #pauseTag(Object)
     * @see com.squareup.picasso.RequestCreator#tag(Object)
     */
    public void resumeTag(Object tag) {
        dispatcher.dispatchResumeTag(tag);
    }

    /**
     * Start an image request using the specified URI.
     * <p/>
     * Passing {@code null} as a {@code uri} will not trigger any request but
     * will set a placeholder, if one is specified.
     *
     * @see #load(java.io.File)
     * @see #load(String)
     * @see #load(int)
     */
    public RequestCreator load(Uri uri) {
        return new RequestCreator(this, uri, 0);
    }

    /**
     * Start an image request using the specified path. This is a convenience
     * method for calling {@link #load(android.net.Uri)}.
     * <p/>
     * This path may be a remote URL, file resource (prefixed with {@code file:}
     * ), content resource (prefixed with {@code content:}), or android resource
     * (prefixed with {@code android.resource:}.
     * <p/>
     * Passing {@code null} as a {@code path} will not trigger any request but
     * will set a placeholder, if one is specified.
     *
     * @throws IllegalArgumentException if {@code path} is empty or blank
     *                                  string.
     * @see #load(android.net.Uri)
     * @see #load(java.io.File)
     * @see #load(int)
     */
    public RequestCreator load(String path) {
        if (path == null || path.trim().length() == 0) {
            return new RequestCreator(this, null, 0);
        }
        return load(Uri.parse(path));
    }

    /**
     * Start an image request using the specified image file. This is a
     * convenience method for calling {@link #load(android.net.Uri)}.
     * <p/>
     * Passing {@code null} as a {@code file} will not trigger any request but
     * will set a placeholder, if one is specified.
     * <p/>
     * Equivalent to calling {@link #load(android.net.Uri) load(Uri.fromFile(file))}.
     *
     * @see #load(android.net.Uri)
     * @see #load(String)
     * @see #load(int)
     */
    public RequestCreator load(File file) {
        if (file == null) {
            return new RequestCreator(this, null, 0);
        }
        return load(Uri.fromFile(file));
    }

    /**
     * Start an image request using the specified drawable resource ID.
     *
     * @see #load(android.net.Uri)
     * @see #load(String)
     * @see #load(java.io.File)
     */
    public RequestCreator load(int resourceId) {
        if (resourceId == 0) {
            throw new IllegalArgumentException("Resource ID must not be zero.");
        }
        return new RequestCreator(this, null, resourceId);
    }

    /**
     * {@code true} if debug display, logging, and statistics are enabled.
     * <p/>
     *
     * @deprecated Use {@link #areIndicatorsEnabled()} and
     * {@link #isLoggingEnabled()} instead.
     */
    @SuppressWarnings("UnusedDeclaration")
    @Deprecated
    public boolean isDebugging() {
        return areIndicatorsEnabled() && isLoggingEnabled();
    }

    /**
     * Toggle whether debug display, logging, and statistics are enabled.
     * <p/>
     *
     * @deprecated Use {@link #setIndicatorsEnabled(boolean)} and
     * {@link #setLoggingEnabled(boolean)} instead.
     */
    @SuppressWarnings("UnusedDeclaration")
    @Deprecated
    public void setDebugging(boolean debugging) {
        setIndicatorsEnabled(debugging);
    }

    /**
     * Toggle whether to display debug indicators on images.
     */
    @SuppressWarnings("UnusedDeclaration")
    public void setIndicatorsEnabled(boolean enabled) {
        indicatorsEnabled = enabled;
    }

    /**
     * {@code true} if debug indicators should are displayed on images.
     */
    @SuppressWarnings("UnusedDeclaration")
    public boolean areIndicatorsEnabled() {
        return indicatorsEnabled;
    }

    /**
     * Toggle whether debug logging is enabled.
     * <p/>
     * <b>WARNING:</b> Enabling this will result in excessive object allocation.
     * This should be only be used for debugging Picasso behavior. Do NOT pass
     * {@code BuildConfig.DEBUG}.
     */
    public void setLoggingEnabled(boolean enabled) {
        loggingEnabled = enabled;
    }

    /**
     * {@code true} if debug logging is enabled.
     */
    public boolean isLoggingEnabled() {
        return loggingEnabled;
    }

    /**
     * Creates a {@link com.squareup.picasso.StatsSnapshot} of the current stats for this instance.
     * <p/>
     * <b>NOTE:</b> The snapshot may not always be completely up-to-date if
     * requests are still in progress.
     */
    @SuppressWarnings("UnusedDeclaration")
    public StatsSnapshot getSnapshot() {
        return stats.createSnapshot();
    }

    /**
     * 清理缓存中包含cacheKey字段的内存
     *
     * @param cacheKey
     */
    public void cleanMemoryCacheByKey(String cacheKey) {
        cache.remove(cacheKey);
    }

    /**
     * 清空缓存
     */
    public void cleanMemoryCache() {
        cache.clear();
    }

    /**
     * 清理缓存文件夹
     */
    public void cleanFileCache() {
        Utils.cleanDefaultCacheDir();
    }

    /**
     * 获取缓存文件夹大小
     *
     * @return
     */
    public long getFileCacheSize() {
        return Utils.calculateDiskCacheSize();
    }

    /**
     * 获取缓存大小，单位为byte
     *
     * @return
     */
    public int getCacheSize() {
        return cache.size();
    }

    /**
     * Stops this instance from accepting further requests.
     */
    public void shutdown() {
        if (this == singleton) {
            throw new UnsupportedOperationException("Default singleton instance cannot be shutdown.");
        }
        if (shutdown) {
            return;
        }
        cache.clear();
        cleanupThread.shutdown();
        stats.shutdown();
        dispatcher.shutdown();
        for (DeferredRequestCreator deferredRequestCreator : targetToDeferredRequestCreator.values()) {
            deferredRequestCreator.cancel();
        }
        targetToDeferredRequestCreator.clear();
        shutdown = true;
    }

    List<RequestHandler> getRequestHandlers() {
        return requestHandlers;
    }

    Request transformRequest(Request request) {
        Request transformed = requestTransformer.transformRequest(request);
        if (transformed == null) {
            throw new IllegalStateException("Request transformer " + requestTransformer.getClass().getCanonicalName()
                    + " returned null for " + request);
        }
        return transformed;
    }

    void defer(ImageView view, DeferredRequestCreator request) {
        targetToDeferredRequestCreator.put(view, request);
    }

    void enqueueAndSubmit(Action action) {
        Object target = action.getTarget();
        if (target != null && targetToAction.get(target) != action) {
            // This will also check we are on the main thread.
            cancelExistingRequest(target);
            targetToAction.put(target, action);
        }
        submit(action);
    }

    void submit(Action action) {
        dispatcher.dispatchSubmit(action);
    }

    Bitmap quickMemoryCacheCheck(String key) {
        Bitmap cached = cache.get(key);
        if (cached != null) {
            stats.dispatchCacheHit();
        } else {
            stats.dispatchCacheMiss();
        }
        return cached;
    }

    void complete(BitmapHunter hunter) {
        Action single = hunter.getAction();
        List<Action> joined = hunter.getActions();

        boolean hasMultiple = joined != null && !joined.isEmpty();
        boolean shouldDeliver = single != null || hasMultiple;

        if (!shouldDeliver) {
            return;
        }

        Uri uri = hunter.getRequest().uri;
        Exception exception = hunter.getException();
        Bitmap result = hunter.getResult();
        LoadedFrom from = hunter.getLoadedFrom();

        if (single != null) {
            deliverAction(result, from, single);
        }

        if (hasMultiple) {
            // noinspection ForLoopReplaceableByForEach
            for (int i = 0, n = joined.size(); i < n; i++) {
                Action join = joined.get(i);
                deliverAction(result, from, join);
            }
        }

        if (listener != null && exception != null) {
            listener.onImageLoadFailed(this, uri, exception);
        }
    }

    void resumeAction(Action action) {
        Bitmap bitmap = null;
        if (!action.skipCache) {
            bitmap = quickMemoryCacheCheck(action.getKey());
        }

        if (bitmap != null) {
            // Resumed action is cached, complete immediately.
            deliverAction(bitmap, MEMORY, action);
            if (loggingEnabled) {
                log(OWNER_MAIN, VERB_COMPLETED, action.request.logId(), "from " + MEMORY);
            }
        } else {
            // Re-submit the action to the executor.
            enqueueAndSubmit(action);
            if (loggingEnabled) {
                log(OWNER_MAIN, VERB_RESUMED, action.request.logId());
            }
        }
    }

    private void deliverAction(Bitmap result, LoadedFrom from, Action action) {
        if (action.isCancelled()) {
            return;
        }
        if (!action.willReplay()) {
            targetToAction.remove(action.getTarget());
        }
        if (result != null) {
            if (from == null) {
                throw new AssertionError("LoadedFrom cannot be null.");
            }
            action.complete(result, from);
            if (loggingEnabled) {
                log(OWNER_MAIN, VERB_COMPLETED, action.request.logId(), "from " + from);
            }
        } else {
            action.error();
            if (loggingEnabled) {
                log(OWNER_MAIN, VERB_ERRORED, action.request.logId());
            }
        }
    }

    private void cancelExistingRequest(Object target) {
        checkMain();
        Action action = targetToAction.remove(target);
        if (action != null) {
            action.cancel();
            dispatcher.dispatchCancel(action);
        }
        if (target instanceof ImageView) {
            ImageView targetImageView = (ImageView) target;
            DeferredRequestCreator deferredRequestCreator = targetToDeferredRequestCreator.remove(targetImageView);
            if (deferredRequestCreator != null) {
                deferredRequestCreator.cancel();
            }
        }
    }

    private static class CleanupThread extends Thread {
        private final ReferenceQueue<?> referenceQueue;
        private final Handler handler;

        CleanupThread(ReferenceQueue<?> referenceQueue, Handler handler) {
            this.referenceQueue = referenceQueue;
            this.handler = handler;
            setDaemon(true);
            setName(THREAD_PREFIX + "refQueue");
        }

        @Override
        public void run() {
            Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
            while (true) {
                try {
                    RequestWeakReference<?> remove = (RequestWeakReference<?>) referenceQueue.remove();
                    handler.sendMessage(handler.obtainMessage(REQUEST_GCED, remove.action));
                } catch (InterruptedException e) {
                    break;
                } catch (final Exception e) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            throw new RuntimeException(e);
                        }
                    });
                    break;
                }
            }
        }

        void shutdown() {
            interrupt();
        }
    }

    /**
     * The global default {@link com.squareup.picasso.Picasso} instance.
     * <p/>
     * This instance is automatically initialized with defaults that are
     * suitable to most implementations.
     * <ul>
     * <li>LRU memory cache of 15% the available application RAM</li>
     * <li>Disk cache of 2% storage space up to 50MB but no less than 5MB.
     * (Note: this is only available on API 14+ <em>or</em> if you are using a
     * standalone library that provides a disk cache on all API levels like
     * OkHttp)</li>
     * <li>Three download threads for disk and network access.</li>
     * </ul>
     * <p/>
     * If these settings do not meet the requirements of your application you
     * can construct your own instance with full control over the configuration
     * by using {@link com.squareup.picasso.Picasso.Builder}.
     */
    public static Picasso with(Context context) {
        if (singleton == null) {
            synchronized (Picasso.class) {
                if (singleton == null) {
                    singleton = new Builder(context).build();
                }
            }
        }
        return singleton;
    }

    /**
     * Fluent API for creating {@link com.squareup.picasso.Picasso} instances.
     */
    @SuppressWarnings("UnusedDeclaration")
    // Public API.
    public static class Builder {
        private final Context context;
        private Downloader downloader;
        private ExecutorService service;
        private Cache cache;
        private Listener listener;
        private RequestTransformer transformer;
        private List<RequestHandler> requestHandlers;

        private boolean indicatorsEnabled;
        private boolean loggingEnabled;

        /**
         * Start building a new {@link com.squareup.picasso.Picasso} instance.
         */
        public Builder(Context context) {
            if (context == null) {
                throw new IllegalArgumentException("Context must not be null.");
            }
            this.context = context.getApplicationContext();
        }

        /**
         * Specify the {@link com.squareup.picasso.Downloader} that will be used for downloading
         * images.
         */
        public Builder downloader(Downloader downloader) {
            if (downloader == null) {
                throw new IllegalArgumentException("Downloader must not be null.");
            }
            if (this.downloader != null) {
                throw new IllegalStateException("Downloader already set.");
            }
            this.downloader = downloader;
            return this;
        }

        /**
         * Specify the executor service for loading images in the background.
         */
        public Builder executor(ExecutorService executorService) {
            if (executorService == null) {
                throw new IllegalArgumentException("Executor service must not be null.");
            }
            if (this.service != null) {
                throw new IllegalStateException("Executor service already set.");
            }
            this.service = executorService;
            return this;
        }

        /**
         * Specify the memory cache used for the most recent images.
         */
        public Builder memoryCache(Cache memoryCache) {
            if (memoryCache == null) {
                throw new IllegalArgumentException("Memory cache must not be null.");
            }
            if (this.cache != null) {
                throw new IllegalStateException("Memory cache already set.");
            }
            this.cache = memoryCache;
            return this;
        }

        /**
         * Specify a listener for interesting events.
         */
        public Builder listener(Listener listener) {
            if (listener == null) {
                throw new IllegalArgumentException("Listener must not be null.");
            }
            if (this.listener != null) {
                throw new IllegalStateException("Listener already set.");
            }
            this.listener = listener;
            return this;
        }

        /**
         * Specify a transformer for all incoming requests.
         * <p/>
         * <b>NOTE:</b> This is a beta feature. The API is subject to change in
         * a backwards incompatible way at any time.
         */
        public Builder requestTransformer(RequestTransformer transformer) {
            if (transformer == null) {
                throw new IllegalArgumentException("Transformer must not be null.");
            }
            if (this.transformer != null) {
                throw new IllegalStateException("Transformer already set.");
            }
            this.transformer = transformer;
            return this;
        }

        public Builder addRequestHandler(RequestHandler requestHandler) {
            if (requestHandler == null) {
                throw new IllegalArgumentException("RequestHandler must not be null.");
            }
            if (requestHandlers == null) {
                requestHandlers = new ArrayList<RequestHandler>();
            }
            if (requestHandlers.contains(requestHandler)) {
                throw new IllegalStateException("RequestHandler already registered.");
            }
            requestHandlers.add(requestHandler);
            return this;
        }

        /**
         * @deprecated Use {@link #indicatorsEnabled(boolean)} instead. Whether
         * debugging is enabled or not.
         */
        @Deprecated
        public Builder debugging(boolean debugging) {
            return indicatorsEnabled(debugging);
        }

        /**
         * Toggle whether to display debug indicators on images.
         */
        public Builder indicatorsEnabled(boolean enabled) {
            this.indicatorsEnabled = enabled;
            return this;
        }

        /**
         * Toggle whether debug logging is enabled.
         * <p/>
         * <b>WARNING:</b> Enabling this will result in excessive object
         * allocation. This should be only be used for debugging purposes. Do
         * NOT pass {@code BuildConfig.DEBUG}.
         */
        public Builder loggingEnabled(boolean enabled) {
            this.loggingEnabled = enabled;
            return this;
        }

        /**
         * Create the {@link com.squareup.picasso.Picasso} instance.
         */
        public Picasso build() {
            Context context = this.context;

            if (downloader == null) {
                downloader = Utils.createDefaultDownloader(context);
            }
            if (cache == null) {
                cache = new LruCache(context);
            }
            if (service == null) {
                service = new PicassoExecutorService();
            }
            if (transformer == null) {
                transformer = RequestTransformer.IDENTITY;
            }

            Stats stats = new Stats(cache);

            Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);

            return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
                    indicatorsEnabled, loggingEnabled);
        }
    }

    /**
     * Describes where the image was loaded from.
     */
    public enum LoadedFrom {
        MEMORY(Color.GREEN), DISK(Color.YELLOW), NETWORK(Color.RED);

        final int debugColor;

        private LoadedFrom(int debugColor) {
            this.debugColor = debugColor;
        }
    }
}
